//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//
//   PACKAGE : dcf
//   FILE    : Behavior.java
//   CLASS   : Behavior
//   DATE    : 2006-04-01
//   
//   DESCRIPTION
//   The Behavior class is an absract class that represents the behavior of a component. It offers operations
//   to register and deregister services with behavior. Registering service will cause the service to be 
//   registerd with the Locator Service when the behavior is initialized (if the behavior is already 
//   initialized the service will be immediately registered with the locator service).
// 
//   Behaviors can be initialized only one time. Subsequent attempts results in errors being logged but no
//   change to the state of the behavior.
//
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//
//   Copyright (c) 2006 by Hal Smith
//
//   Permission is hereby granted, free of charge, to any person obtaining a copy of this source code 
//   (the "Code")to deal with the Code without restriction, including without limitation the rights to use, copy,
//   modify, merge, publish and distribute the Code, and to permit persons to whom the Code is furnished to do 
//   so, subject to the following conditions:
//
//   The above copyright notice and this permission notice shall be included in all copies or substantial 
//   portions of the Software.
//   
//
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//
//   VERSION HISTORY
//
//
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

package dcf;

import java.util.HashMap;
import java.util.Collection;
import java.util.concurrent.*;


public abstract class Behavior {
	
	// A flag to indicate if initialize() operation has been called (true) or not (false).
	//
	private boolean isInitialized;
	
	
	// Since the behavior can only process a single message at a time, the behavior uses a message queue to
	// hold messages while it is busy processing a previous request. Since the listener is threaded, we need
	// to use a blocking queue.
	//
	private Thread                       incomingMessageQueueListener;
	private LinkedBlockingQueue<Message> incomingMessageQueue;
	
	
	// The outgoing behavior used by this behavior to send messages.
	//
	private IOutgoingBehavior outgoingBehavior;
	
	
    // There can only be a single service registered for a given service name. This hashtable tracks service
	// names with associated services.
	//
	private HashMap<String, Service> services;

	
	// The endpoints for the locator service (used to register services) and the behavior endpoint (used
	// to indicate a return address when sending messages).
	//
	private Endpoint lsEndpoint;
	Endpoint behaviorEndpoint;
	
	
	// The QueueListener is intended to be a threaded object that waits for messages in the incoming message
	// queue and handles them based on their type.
	//
	private class QueueListener implements Runnable {

		// This implements the run() operation defined on Runnable. The operation blocks waiting for a message
		// to arrive in the incoming message queue. When a message arrives, the operation removes it from the
		// queue for processing. There are no exceptions thrown by this operation.
		//
		public void run() {
			
			while(true) {
				
				try {
					
					// Take the next available message from the queue (which is blocking).
					//
					Message incomingMessage = incomingMessageQueue.take();
					
					
					// If the message contains a locator service related message (i.e., subscriber, 
					// subscriberList, publisher, disconnect) handle it here. 
					// 
					// Note that errors will be generated if one of these messages are received for a 
					// service this behavior is not aware. The arrival of such a message would represent 
					// either an error in the locator service or in the system configuration - i.e., it's 
					// EXTREMELY fatal. 
					//
					// If the message of any other type forward it along. 
					// 
					// SUBSCRIBER MESSAGE
					if (incomingMessage instanceof SubscriberMessage) {
						
						// Capture the subscriber message and its defined service name.
						//
						SubscriberMessage message     = (SubscriberMessage)incomingMessage;
						String            serviceName = message.getSubscriber().getServiceName();
						
						
						// Add the subsriber to the serivce. If the service is not known to the behavior, 
						// register an error. 
						//
						if (services.get(serviceName) == null) {
							
							System.out.println(getClass().getName() + ".run():");
							System.out.println("   Received SubscriberMessage for a non-registered service :"
									           + message.getServiceName());

						} else {
							
							services.get(serviceName).addRemoteEndpoint(message.getSubscriber());
							
							interpretMessage(new BindNotificationMessage(serviceName));

						}

					// 
					// SUBSCRIBER LIST MESSAGE
					//
					} else if(incomingMessage instanceof SubscriberListMessage) {

						//Capture the subscriber list message and its defined service name.
						//
						SubscriberListMessage message     = (SubscriberListMessage)incomingMessage;
						String                serviceName = message.getServiceName();
						
						
						// Add each subsriber in the list to the serivce. If the service is not known to the 
						// behavior, register an error. 
						//
						if (services.get(serviceName) == null) {
							
							System.out.println(getClass().getName() + ".run():");
							System.out.println("   Received SubscriberListMessage for a non-registered service :"
									           + message.getServiceName());

						} else {
							
							for (int i=0; i<message.getSubscribers().getSubscriptionCount(); ++i) {
								services.get(serviceName).addRemoteEndpoint(message.getSubscribers().getSubscriber(i));
								interpretMessage(new BindNotificationMessage(serviceName));
							}

						}
					
					//
					// PUBLISHER MESSAGE
					//
					} else if(incomingMessage instanceof PublisherMessage) {
						
						//Capture the publisher message and its defined service name.
						//
						PublisherMessage message     = (PublisherMessage)incomingMessage;
						String           serviceName = message.getServiceName();
						
						// Add the publisher endpoint to the serivce. If the service is not 
						// known to the behavior, register an error. 
						//
						if (services.get(serviceName) == null) {
							
							System.out.println(getClass().getName() + ".run():");
							System.out.println("   Received PublisherMessage for a non-registered service :" + message.getServiceName());

						} else {

							services.get(serviceName).addRemoteEndpoint(message.getPublisher());

							BindNotificationMessage bnm = new BindNotificationMessage(serviceName);
							
							bnm.setSenderEndpoint(message.getSenderEndpoint());
							
							interpretMessage(new BindNotificationMessage(serviceName));
							
						}

						
					//
					// DISCONNECT MESSAGE
					//
					} else if (incomingMessage instanceof DisconnectMessage) {
						
						
						//Capture the disconnect message.
						//
						DisconnectMessage message = (DisconnectMessage)incomingMessage;
						
						
						// Locate the associated service in the list and remove the endpoint specified in the
						// message. Then forward an UnboundMessage to the subclass. Log an error if the service
						// is unknown to the behavior.
						// 
						Service serviceToModify = services.get(message.getServiceName());
						
						if (serviceToModify == null) {
							
							System.out.println(getClass().getName() + ".run():");
							System.out.println("   Received a disconnect message for a non-registered service :" 
									           + message.getServiceName());
							
						} else {
							
							serviceToModify.removeRemoteEndpoint(message.getEndpoint());
							
							UnboundNotificationMessage unm = 
								                       new UnboundNotificationMessage(message.getServiceName());
							
							unm.setSenderEndpoint(message.getEndpoint());
							
							interpretMessage(unm);
							
						}
						
						
					// 
					// ANY NON-LOCATOR SERVICE RELATED MESSAGE
					//
					} else {
						
						interpretMessage(incomingMessage);
					
					} // if-else
				

				} catch(Exception e) {

					System.out.println(getClass().getName() + ".run():");
					System.out.println("   Unexpected exception :" + e.toString());
				
				} // try-catch
				
			} // while

		} // run()
	
	} // QueueListener

	
	
	// Constructor.
	//
	// The default constructor creates a behavior without an associated endpoint or locator service. This 
	// is only used for behaviors defined within the package that do not use the locator service (e.g., the
	// locator service itself.) The only exception thrown from this operation is InitializationException. In
	// that case an error is logged prior to the exception being thrown.
	//
	Behavior() throws InitializationException {
		
		try {
		
			// Initialize the initialization flag to indicate that initialize() has not yet been called.
			//
			isInitialized = false;
			
			// Create the queue and the queue listener. It executes on a separate thread so we need to use a 
			// blocking queue.
			//
			incomingMessageQueue         = new LinkedBlockingQueue<Message>();
			incomingMessageQueueListener = new Thread(new QueueListener());
			incomingMessageQueueListener.start();
	
			// Create the hashtable for storing the services.
			//
			services = new HashMap<String, Service>();
		
		} catch (Exception e) {
			
	    		
			System.out.println("Behavior.Behavior():");
			System.out.println("   Unexpected exception occured: " + e.toString());
			
			throw new InitializationException();
		
		}
	
	} // constructor

	
	// Constructor.
	//
	// This operation creates a behavior that uses the specified locator service for registering services and
	// the behavior endpoint as a return address in messages it generates. If either of the specified endpoints
	// are null, the constructor throws InitializationException. There are no other exceptions thrown by this
	// operation.
	//
	public Behavior(Endpoint initLSEndpoint, Endpoint initBehaviorEndpoint) throws InitializationException {
		
		// For convenience, invoke the default constructor.
		this();
		
		lsEndpoint       = initLSEndpoint;
		behaviorEndpoint = initBehaviorEndpoint;

		// The remainder of this operation ensures that the endpoints provided are not null. If they are, the
		// appropriate error is logged.
		//
		boolean lsEndpointNull       = (initLSEndpoint == null);
		
		
		if (initLSEndpoint       == null ||
		    initBehaviorEndpoint == null) {
				
			System.out.println("Behavior.Behavior(Endpoint, Endpoint):");
			System.out.println("   One of the supplied parameters is null.");
			System.out.println("   Supplied local endpoint null?    " + (initLSEndpoint       == null));
			System.out.println("   Supplied behavior endpoint null? " + (initBehaviorEndpoint == null));
			
			throw new InitializationException();
   					
		}
		
	} // constructor
	

	
	// This operation adds the service to the behavior. If the behavior has already been initialized, the 
	// service is automatically registered with the locator service. If not, the service will
	// be registered upon initialization. If the service name associated to the service provided is already
	// known to the behavior, the supplied service will replace the current one.
	//
	// There are no exceptions thrown by the operation.
	//
	 void addService(IService service) {
		
		try {
		
			// TODO This is potentially fragile. The issue is that Behavior needs to set the local endpoint but 
			// we want such ability to be invisible to the user. For now, test the object's type. Since 
			// the IService factory is in the container, this shouldn't be an issue.
			//
			if (service instanceof Service) {
				System.out.println("addService");
				
				// If valid, set the local endpoint and outgoing behvaior to that currently known by the 
				// behavior. We could just set the behaviors to null but when the system is first coming up,
				// there is the potential for services to be added before the endpoint and behaviors are 
				// set. Since we opted for flexibility and used the accessor/mutator approach, we just perform
				// this check.
				// 
				if (behaviorEndpoint != null) {
					System.out.println("behaviorEndpoint != null");
					((Service)service).setLocalEndpoint(behaviorEndpoint);
				}
				
				if (outgoingBehavior != null) {
					System.out.println("outgoingBehavior != null");
					((Service)service).setOutgoingBehavior(outgoingBehavior);
				}
				
				
				// If the service name has already been registered with the behavior, log a message 
				// indicating that to be the case and that the current service will be replaced with the
				// new service.
				//
				if (services.get(service.getServiceName()) != null) {
					
					System.out.println(getClass().getName() + ".addService():");
					System.out.println("   Multiple services have registered with service name " 
							           + service.getServiceName());
					System.out.println("   Using latest service added.");
									
				}
				
		        // Place the service in the service hash. Then, if the behavior has been initialized, register
				// the service.
				// 
				services.put(service.getServiceName(), (Service)service);
		        
				if (isInitialized) {
		
					registerService(service);
		
				}
			
			} else {
				
				System.out.println(getClass().getName() + ".addService():");
				System.out.println("   Expected subclass Service of IService. Received subclass " 
						           + service.getClass().getName());
				
			} // if (service instanceof Service) ...
		
		} catch (Exception e) {
			
			System.out.println(getClass().getName() + ".addService(IService):");
			System.out.println("   Unexpected exception :" + e.toString());
			
		} // try-catch

	} // addService()


	
	// This operation deregisters the service from both the local behavior hash as well from the locator 
	// service. If the service identified is not known to the behavior, no action is taken. 
	//
	// Note: This does not remove a service by name but by physical service. That means that if the behavior
	// has a service associated to the supplied service's service name BUT the two physical services are not 
	// identical, the service is not removed. (A service may be replaced if a second service is registered
	// with the same service name.)
	// 
	// There are no exceptions thrown from this operation.
	//
	
	protected void deregisterService(IService service) {
		
		try {
		
			// TODO This is potentially fragile. We want to ensure that we are working with the right type of
			// IService - specifically, one of type Service. Since the IService factory is in the container, 
			// but we test it out of good measure.
			//
			if (service instanceof Service) {
	
				// Look for the service in the locally known services by name. If no service is associated to
				// the service name, log a message indicating so. If there is a service but it's not the same
				// physical service as trying to be removed, log an error and leave the current service in 
				// place. If the service is the one currently in place, remove it from the local list of 
				// services and send a disconnect message to the locator service.
				//
				Service serviceToRemove = services.get(service.getServiceName());
							
				if (serviceToRemove == null) {
					
					System.out.println(getClass().getName() + ".deregisterService(IService):");
					System.out.println("   Request to deregister an unregistered service : " 
							           + service.getServiceName());
									
				} else if (serviceToRemove != service) { 
				
					System.out.println(getClass().getName() + ".deregisterService(IService):");
					System.out.println("   Service name known but physical service is different for : " 
							           + service.getServiceName());

				} else {
				
			        services.remove(serviceToRemove.getServiceName());
			        serviceToRemove.clearRemoteEndpoints();
	
					DeregisterMessage dm = new DeregisterMessage(serviceToRemove.getServiceName());
					dm.setSenderEndpoint(behaviorEndpoint);
					outgoingBehavior.sendTo(lsEndpoint, dm);		
	
				} // if (serviceToRemove == null) ...
			
			} else {
				
				System.out.println(getClass().getName() + ".deregisterService(IService):");
				System.out.println("   Expected subclass Service of IService. Received subclass " + service.getClass().getName());
				
			} // if (service instanceof Service) ...

			
		} catch (Exception e) {
			
			System.out.println(getClass().getName() + ".deregisterService(IService):");
			System.out.println("   Unexpected exception :" + e.toString());
			
		} // try-catch
		
		
	} // deregisterService()
	

	
	// This message adds a message to the behavior's incoming message queue. There are no exceptions thrown
	// from this operation.
	// 
	//message queue.
	protected void addMessage(Envelope newEnvelope) { 
		
		try {
			
		    // Place the message in the incoming message queue.
			//
			incomingMessageQueue.put(newEnvelope.getMessage());
		
		} catch (Exception e) {
			
			System.out.println(getClass().getName() + ".addMessage(Envelope):");
			System.out.println("   Unexpected exception :" + e.toString());
		
		}

	} // addMessage()

	
	
	// This operation updates the behavior to use the specified outgoing behavior for sending 
	// messages. There is no exception handling performed by this operation.
	//
	protected void setOutgoingBehavior(IOutgoingBehavior newOutgoingBehavior)  {
		
		// Save the specified outgoing behavior locally and update all services to use it.
		//
		outgoingBehavior = newOutgoingBehavior;
		
		Collection<Service> servs = services.values();
		
		for (Service s : servs) {
			s.setOutgoingBehavior(outgoingBehavior);
		}
	
	} // setOutgoingBehavior()
	
	
	
	// This operation is a factory method for creating new services. If there is a problem creating the
	// service, an error is logged and null is returned. There are no exceptions thrown by this operation 
	//
	public IService createService( String serviceName, IService.ServiceType serviceType) {
		
		IService result = null;
		
		try {
			
		   result = new Service(serviceName, serviceType, behaviorEndpoint);

		   addService(result);
			   
		} catch (Exception e) {
			
			System.out.println("Behavior.createService(String, IService.ServiceType):");
			System.out.println("   Unexpected exception occured : " + e.toString());
			System.out.println("   Service requested      :"  + serviceName);
			System.out.println("   Service type requested : " + serviceType);
						
		}
		
		return result;
		
	} // createService()
	


	// This operation initializes the behavior. Attempting to initialize a behavior more than once results 
	// in the subsequent requests being ignored and an error being logged. There are no exceptions thrown
	// by this operation. If the behavior fails to initialize, an error is logged.
	//
	void initialize() {
		
		try {

			// If the behavior has not been initialized, do so. Otherwise, log an error.
			//
			if (!isInitialized) {
			
				// Toggle the flag to indicate that initialization has already taken place and register all 
				// services currently known by the behavior.
				//
				isInitialized = true;
				
				if (!services.isEmpty()) {
					Collection<Service> servs = services.values();
		
					for(Service s : servs) {
			            registerService(s);
					}
				
				}
			
			} else {
				
				System.out.println(getClass().getName() + ".initialize():");
				System.out.println("   Initialize called more than once.");
	
			}
			
		} catch (Exception e) {
			
			System.out.println(getClass().getName() + ".initialize():");
			System.out.println("   Unexpected exception: " + e.toString());
						
		} // try-catch

	} // initialize()
	
	
	
	// This operation registers services with the locator service. The operation will return true if 
	// registration is successful and false otherwise. The provided service is assumed to have a non-null
	// service name. There are no exceptions thrown by this operation.
	//
	private boolean registerService(IService service) {
		
		boolean result = false;
		
		// Depending on whether the service is a publisher or subscriber, create the appropriate request
		// and send it to the locator service.
		//
		try {
			switch (service.getServiceType()) {
			case SUBSCRIBER:
				
				SubscribeRequest subMessage = new SubscribeRequest(service.getServiceName());
				subMessage.setSenderEndpoint(behaviorEndpoint);
				result = outgoingBehavior.sendTo(lsEndpoint, subMessage);		
				break;
				
			case PUBLISHER:
				
				PublishRequest pubMessage = new PublishRequest(service.getServiceName());
				pubMessage.setSenderEndpoint(behaviorEndpoint);
				outgoingBehavior.sendTo(lsEndpoint, pubMessage);		
				break;
			
			} // switch
		
		} catch (Exception e) {
			
			// There should be no exceptions. The SubscribeRequest and PublishRequest operations can throw
			// an InitializationException but that should only occur if the service name is null. A service
			// object should not be constructed with a null service name.
			//
			System.out.println("Behavior.registerService(service):");
			System.out.println("   Unexpected exception: " + e.toString());
			
		} // try-catch
		

		return result;

		
	} // registerService()


	// This abstract operation is the message handler for the behavior. As messages arrive, they are passed to
	// this operation to be handled. It is recommmened that any implementation of this operation have a default
	// handler for unexpected messages. 
	//
	// In addition to user-defined messages, there are system level messages that will passed to this operation:
	//
	//    BindNotificationMessage    - Used to indicate that a service has been bound to a remote endpoint. This 
	//                                 indicates communication is now possible.
	//
	//    UnboundNotificationMessage - Used to indicate that a service has been disconnected from a remote 
	//                                 endpoint. Communication is no longer possible through this service to 
	//                                 the endpoint.
    //
	//
	abstract public void interpretMessage(Message message);

	
} // Behavior